// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package nginx.clojure.asm.commons;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import nginx.clojure.asm.ConstantDynamic;
import nginx.clojure.asm.Handle;
import nginx.clojure.asm.Label;
import nginx.clojure.asm.MethodVisitor;
import nginx.clojure.asm.Opcodes;
import nginx.clojure.asm.Type;

/**
 * A {@link MethodVisitor} that keeps track of stack map frame changes between {@link
 * #visitFrame(int, int, Object[], int, Object[])} calls. This adapter must be used with the {@link
 * nginx.clojure.asm.ClassReader#EXPAND_FRAMES} option. Each visit<i>X</i> instruction delegates to
 * the next visitor in the chain, if any, and then simulates the effect of this instruction on the
 * stack map frame, represented by {@link #locals} and {@link #stack}. The next visitor in the chain
 * can get the state of the stack map frame <i>before</i> each instruction by reading the value of
 * these fields in its visit<i>X</i> methods (this requires a reference to the AnalyzerAdapter that
 * is before it in the chain). If this adapter is used with a class that does not contain stack map
 * table attributes (i.e., pre Java 6 classes) then this adapter may not be able to compute the
 * stack map frame for each instruction. In this case no exception is thrown but the {@link #locals}
 * and {@link #stack} fields will be null for these instructions.
 *
 * @author Eric Bruneton
 */
public class AnalyzerAdapter extends MethodVisitor {

  /**
   * The local variable slots for the current execution frame. Primitive types are represented by
   * {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link Opcodes#FLOAT}, {@link Opcodes#LONG},
   * {@link Opcodes#DOUBLE},{@link Opcodes#NULL} or {@link Opcodes#UNINITIALIZED_THIS} (long and
   * double are represented by two elements, the second one being TOP). Reference types are
   * represented by String objects (representing internal names, see {@link
   * Type#getInternalName()}), and uninitialized types by Label objects (this label designates the
   * NEW instruction that created this uninitialized value). This field is {@literal null} for
   * unreachable instructions.
   */
  public List<Object> locals;

  /**
   * The operand stack slots for the current execution frame. Primitive types are represented by
   * {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link Opcodes#FLOAT}, {@link Opcodes#LONG},
   * {@link Opcodes#DOUBLE},{@link Opcodes#NULL} or {@link Opcodes#UNINITIALIZED_THIS} (long and
   * double are represented by two elements, the second one being TOP). Reference types are
   * represented by String objects (representing internal names, see {@link
   * Type#getInternalName()}), and uninitialized types by Label objects (this label designates the
   * NEW instruction that created this uninitialized value). This field is {@literal null} for
   * unreachable instructions.
   */
  public List<Object> stack;

  /** The labels that designate the next instruction to be visited. May be {@literal null}. */
  private List<Label> labels;

  /**
   * The uninitialized types in the current execution frame. This map associates internal names to
   * Label objects (see {@link Type#getInternalName()}). Each label designates a NEW instruction
   * that created the currently uninitialized types, and the associated internal name represents the
   * NEW operand, i.e. the final, initialized type value.
   */
  public Map<Object, Object> uninitializedTypes;

  /** The maximum stack size of this method. */
  private int maxStack;

  /** The maximum number of local variables of this method. */
  private int maxLocals;

  /** The owner's class name. */
  private String owner;

  /**
   * Constructs a new {@link AnalyzerAdapter}. <i>Subclasses must not use this constructor</i>.
   * Instead, they must use the {@link #AnalyzerAdapter(int, String, int, String, String,
   * MethodVisitor)} version.
   *
   * @param owner the owner's class name.
   * @param access the method's access flags (see {@link Opcodes}).
   * @param name the method's name.
   * @param descriptor the method's descriptor (see {@link Type}).
   * @param methodVisitor the method visitor to which this adapter delegates calls. May be {@literal
   *     null}.
   * @throws IllegalStateException If a subclass calls this constructor.
   */
  public AnalyzerAdapter(
      final String owner,
      final int access,
      final String name,
      final String descriptor,
      final MethodVisitor methodVisitor) {
    this(/* latest api = */ Opcodes.ASM9, owner, access, name, descriptor, methodVisitor);
    if (getClass() != AnalyzerAdapter.class) {
      throw new IllegalStateException();
    }
  }

  /**
   * Constructs a new {@link AnalyzerAdapter}.
   *
   * @param api the ASM API version implemented by this visitor. Must be one of the {@code
   *     ASM}<i>x</i> values in {@link Opcodes}.
   * @param owner the owner's class name.
   * @param access the method's access flags (see {@link Opcodes}).
   * @param name the method's name.
   * @param descriptor the method's descriptor (see {@link Type}).
   * @param methodVisitor the method visitor to which this adapter delegates calls. May be {@literal
   *     null}.
   */
  protected AnalyzerAdapter(
      final int api,
      final String owner,
      final int access,
      final String name,
      final String descriptor,
      final MethodVisitor methodVisitor) {
    super(api, methodVisitor);
    this.owner = owner;
    locals = new ArrayList<>();
    stack = new ArrayList<>();
    uninitializedTypes = new HashMap<>();

    if ((access & Opcodes.ACC_STATIC) == 0) {
      if ("<init>".equals(name)) {
        locals.add(Opcodes.UNINITIALIZED_THIS);
      } else {
        locals.add(owner);
      }
    }
    for (Type argumentType : Type.getArgumentTypes(descriptor)) {
      switch (argumentType.getSort()) {
        case Type.BOOLEAN:
        case Type.CHAR:
        case Type.BYTE:
        case Type.SHORT:
        case Type.INT:
          locals.add(Opcodes.INTEGER);
          break;
        case Type.FLOAT:
          locals.add(Opcodes.FLOAT);
          break;
        case Type.LONG:
          locals.add(Opcodes.LONG);
          locals.add(Opcodes.TOP);
          break;
        case Type.DOUBLE:
          locals.add(Opcodes.DOUBLE);
          locals.add(Opcodes.TOP);
          break;
        case Type.ARRAY:
          locals.add(argumentType.getDescriptor());
          break;
        case Type.OBJECT:
          locals.add(argumentType.getInternalName());
          break;
        default:
          throw new AssertionError();
      }
    }
    maxLocals = locals.size();
  }

  @Override
  public void visitFrame(
      final int type,
      final int numLocal,
      final Object[] local,
      final int numStack,
      final Object[] stack) {
    if (type != Opcodes.F_NEW) { // Uncompressed frame.
      throw new IllegalArgumentException(
          "AnalyzerAdapter only accepts expanded frames (see ClassReader.EXPAND_FRAMES)");
    }

    super.visitFrame(type, numLocal, local, numStack, stack);

    if (this.locals != null) {
      this.locals.clear();
      this.stack.clear();
    } else {
      this.locals = new ArrayList<>();
      this.stack = new ArrayList<>();
    }
    visitFrameTypes(numLocal, local, this.locals);
    visitFrameTypes(numStack, stack, this.stack);
    maxLocals = Math.max(maxLocals, this.locals.size());
    maxStack = Math.max(maxStack, this.stack.size());
  }

  private static void visitFrameTypes(
      final int numTypes, final Object[] frameTypes, final List<Object> result) {
    for (int i = 0; i < numTypes; ++i) {
      Object frameType = frameTypes[i];
      result.add(frameType);
      if (frameType == Opcodes.LONG || frameType == Opcodes.DOUBLE) {
        result.add(Opcodes.TOP);
      }
    }
  }

  @Override
  public void visitInsn(final int opcode) {
    super.visitInsn(opcode);
    execute(opcode, 0, null);
    if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
      this.locals = null;
      this.stack = null;
    }
  }

  @Override
  public void visitIntInsn(final int opcode, final int operand) {
    super.visitIntInsn(opcode, operand);
    execute(opcode, operand, null);
  }

  @Override
  public void visitVarInsn(final int opcode, final int varIndex) {
    super.visitVarInsn(opcode, varIndex);
    boolean isLongOrDouble =
        opcode == Opcodes.LLOAD
            || opcode == Opcodes.DLOAD
            || opcode == Opcodes.LSTORE
            || opcode == Opcodes.DSTORE;
    maxLocals = Math.max(maxLocals, varIndex + (isLongOrDouble ? 2 : 1));
    execute(opcode, varIndex, null);
  }

  @Override
  public void visitTypeInsn(final int opcode, final String type) {
    if (opcode == Opcodes.NEW) {
      if (labels == null) {
        Label label = new Label();
        labels = new ArrayList<>(3);
        labels.add(label);
        if (mv != null) {
          mv.visitLabel(label);
        }
      }
      for (Label label : labels) {
        uninitializedTypes.put(label, type);
      }
    }
    super.visitTypeInsn(opcode, type);
    execute(opcode, 0, type);
  }

  @Override
  public void visitFieldInsn(
      final int opcode, final String owner, final String name, final String descriptor) {
    super.visitFieldInsn(opcode, owner, name, descriptor);
    execute(opcode, 0, descriptor);
  }

  @Override
  public void visitMethodInsn(
      final int opcodeAndSource,
      final String owner,
      final String name,
      final String descriptor,
      final boolean isInterface) {
    if (api < Opcodes.ASM5 && (opcodeAndSource & Opcodes.SOURCE_DEPRECATED) == 0) {
      // Redirect the call to the deprecated version of this method.
      super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
      return;
    }
    super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
    int opcode = opcodeAndSource & ~Opcodes.SOURCE_MASK;

    if (this.locals == null) {
      labels = null;
      return;
    }
    pop(descriptor);
    if (opcode != Opcodes.INVOKESTATIC) {
      Object value = pop();
      if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
        Object initializedValue;
        if (value == Opcodes.UNINITIALIZED_THIS) {
          initializedValue = this.owner;
        } else {
          initializedValue = uninitializedTypes.get(value);
        }
        for (int i = 0; i < locals.size(); ++i) {
          if (locals.get(i) == value) {
            locals.set(i, initializedValue);
          }
        }
        for (int i = 0; i < stack.size(); ++i) {
          if (stack.get(i) == value) {
            stack.set(i, initializedValue);
          }
        }
      }
    }
    pushDescriptor(descriptor);
    labels = null;
  }

  @Override
  public void visitInvokeDynamicInsn(
      final String name,
      final String descriptor,
      final Handle bootstrapMethodHandle,
      final Object... bootstrapMethodArguments) {
    super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
    if (this.locals == null) {
      labels = null;
      return;
    }
    pop(descriptor);
    pushDescriptor(descriptor);
    labels = null;
  }

  @Override
  public void visitJumpInsn(final int opcode, final Label label) {
    super.visitJumpInsn(opcode, label);
    execute(opcode, 0, null);
    if (opcode == Opcodes.GOTO) {
      this.locals = null;
      this.stack = null;
    }
  }

  @Override
  public void visitLabel(final Label label) {
    super.visitLabel(label);
    if (labels == null) {
      labels = new ArrayList<>(3);
    }
    labels.add(label);
  }

  @Override
  public void visitLdcInsn(final Object value) {
    super.visitLdcInsn(value);
    if (this.locals == null) {
      labels = null;
      return;
    }
    if (value instanceof Integer) {
      push(Opcodes.INTEGER);
    } else if (value instanceof Long) {
      push(Opcodes.LONG);
      push(Opcodes.TOP);
    } else if (value instanceof Float) {
      push(Opcodes.FLOAT);
    } else if (value instanceof Double) {
      push(Opcodes.DOUBLE);
      push(Opcodes.TOP);
    } else if (value instanceof String) {
      push("java/lang/String");
    } else if (value instanceof Type) {
      int sort = ((Type) value).getSort();
      if (sort == Type.OBJECT || sort == Type.ARRAY) {
        push("java/lang/Class");
      } else if (sort == Type.METHOD) {
        push("java/lang/invoke/MethodType");
      } else {
        throw new IllegalArgumentException();
      }
    } else if (value instanceof Handle) {
      push("java/lang/invoke/MethodHandle");
    } else if (value instanceof ConstantDynamic) {
      pushDescriptor(((ConstantDynamic) value).getDescriptor());
    } else {
      throw new IllegalArgumentException();
    }
    labels = null;
  }

  @Override
  public void visitIincInsn(final int varIndex, final int increment) {
    super.visitIincInsn(varIndex, increment);
    maxLocals = Math.max(maxLocals, varIndex + 1);
    execute(Opcodes.IINC, varIndex, null);
  }

  @Override
  public void visitTableSwitchInsn(
      final int min, final int max, final Label dflt, final Label... labels) {
    super.visitTableSwitchInsn(min, max, dflt, labels);
    execute(Opcodes.TABLESWITCH, 0, null);
    this.locals = null;
    this.stack = null;
  }

  @Override
  public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
    super.visitLookupSwitchInsn(dflt, keys, labels);
    execute(Opcodes.LOOKUPSWITCH, 0, null);
    this.locals = null;
    this.stack = null;
  }

  @Override
  public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) {
    super.visitMultiANewArrayInsn(descriptor, numDimensions);
    execute(Opcodes.MULTIANEWARRAY, numDimensions, descriptor);
  }

  @Override
  public void visitLocalVariable(
      final String name,
      final String descriptor,
      final String signature,
      final Label start,
      final Label end,
      final int index) {
    char firstDescriptorChar = descriptor.charAt(0);
    maxLocals =
        Math.max(
            maxLocals, index + (firstDescriptorChar == 'J' || firstDescriptorChar == 'D' ? 2 : 1));
    super.visitLocalVariable(name, descriptor, signature, start, end, index);
  }

  @Override
  public void visitMaxs(final int maxStack, final int maxLocals) {
    if (mv != null) {
      this.maxStack = Math.max(this.maxStack, maxStack);
      this.maxLocals = Math.max(this.maxLocals, maxLocals);
      mv.visitMaxs(this.maxStack, this.maxLocals);
    }
  }

  // -----------------------------------------------------------------------------------------------

  private Object get(final int local) {
    maxLocals = Math.max(maxLocals, local + 1);
    return local < locals.size() ? locals.get(local) : Opcodes.TOP;
  }

  private void set(final int local, final Object type) {
    maxLocals = Math.max(maxLocals, local + 1);
    while (local >= locals.size()) {
      locals.add(Opcodes.TOP);
    }
    locals.set(local, type);
  }

  private void push(final Object type) {
    stack.add(type);
    maxStack = Math.max(maxStack, stack.size());
  }

  private void pushDescriptor(final String fieldOrMethodDescriptor) {
    String descriptor =
        fieldOrMethodDescriptor.charAt(0) == '('
            ? Type.getReturnType(fieldOrMethodDescriptor).getDescriptor()
            : fieldOrMethodDescriptor;
    switch (descriptor.charAt(0)) {
      case 'V':
        return;
      case 'Z':
      case 'C':
      case 'B':
      case 'S':
      case 'I':
        push(Opcodes.INTEGER);
        return;
      case 'F':
        push(Opcodes.FLOAT);
        return;
      case 'J':
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        return;
      case 'D':
        push(Opcodes.DOUBLE);
        push(Opcodes.TOP);
        return;
      case '[':
        push(descriptor);
        break;
      case 'L':
        push(descriptor.substring(1, descriptor.length() - 1));
        break;
      default:
        throw new AssertionError();
    }
  }

  private Object pop() {
    return stack.remove(stack.size() - 1);
  }

  private void pop(final int numSlots) {
    int size = stack.size();
    int end = size - numSlots;
    for (int i = size - 1; i >= end; --i) {
      stack.remove(i);
    }
  }

  private void pop(final String descriptor) {
    char firstDescriptorChar = descriptor.charAt(0);
    if (firstDescriptorChar == '(') {
      int numSlots = 0;
      Type[] types = Type.getArgumentTypes(descriptor);
      for (Type type : types) {
        numSlots += type.getSize();
      }
      pop(numSlots);
    } else if (firstDescriptorChar == 'J' || firstDescriptorChar == 'D') {
      pop(2);
    } else {
      pop(1);
    }
  }

  private void execute(final int opcode, final int intArg, final String stringArg) {
    if (opcode == Opcodes.JSR || opcode == Opcodes.RET) {
      throw new IllegalArgumentException("JSR/RET are not supported");
    }
    if (this.locals == null) {
      labels = null;
      return;
    }
    Object value1;
    Object value2;
    Object value3;
    Object t4;
    switch (opcode) {
      case Opcodes.NOP:
      case Opcodes.INEG:
      case Opcodes.LNEG:
      case Opcodes.FNEG:
      case Opcodes.DNEG:
      case Opcodes.I2B:
      case Opcodes.I2C:
      case Opcodes.I2S:
      case Opcodes.GOTO:
      case Opcodes.RETURN:
        break;
      case Opcodes.ACONST_NULL:
        push(Opcodes.NULL);
        break;
      case Opcodes.ICONST_M1:
      case Opcodes.ICONST_0:
      case Opcodes.ICONST_1:
      case Opcodes.ICONST_2:
      case Opcodes.ICONST_3:
      case Opcodes.ICONST_4:
      case Opcodes.ICONST_5:
      case Opcodes.BIPUSH:
      case Opcodes.SIPUSH:
        push(Opcodes.INTEGER);
        break;
      case Opcodes.LCONST_0:
      case Opcodes.LCONST_1:
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        break;
      case Opcodes.FCONST_0:
      case Opcodes.FCONST_1:
      case Opcodes.FCONST_2:
        push(Opcodes.FLOAT);
        break;
      case Opcodes.DCONST_0:
      case Opcodes.DCONST_1:
        push(Opcodes.DOUBLE);
        push(Opcodes.TOP);
        break;
      case Opcodes.ILOAD:
      case Opcodes.FLOAD:
      case Opcodes.ALOAD:
        push(get(intArg));
        break;
      case Opcodes.LLOAD:
      case Opcodes.DLOAD:
        push(get(intArg));
        push(Opcodes.TOP);
        break;
      case Opcodes.LALOAD:
      case Opcodes.D2L:
        pop(2);
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        break;
      case Opcodes.DALOAD:
      case Opcodes.L2D:
        pop(2);
        push(Opcodes.DOUBLE);
        push(Opcodes.TOP);
        break;
      case Opcodes.AALOAD:
        pop(1);
        value1 = pop();
        if (value1 instanceof String) {
          pushDescriptor(((String) value1).substring(1));
        } else if (value1 == Opcodes.NULL) {
          push(value1);
        } else {
          push("java/lang/Object");
        }
        break;
      case Opcodes.ISTORE:
      case Opcodes.FSTORE:
      case Opcodes.ASTORE:
        value1 = pop();
        set(intArg, value1);
        if (intArg > 0) {
          value2 = get(intArg - 1);
          if (value2 == Opcodes.LONG || value2 == Opcodes.DOUBLE) {
            set(intArg - 1, Opcodes.TOP);
          }
        }
        break;
      case Opcodes.LSTORE:
      case Opcodes.DSTORE:
        pop(1);
        value1 = pop();
        set(intArg, value1);
        set(intArg + 1, Opcodes.TOP);
        if (intArg > 0) {
          value2 = get(intArg - 1);
          if (value2 == Opcodes.LONG || value2 == Opcodes.DOUBLE) {
            set(intArg - 1, Opcodes.TOP);
          }
        }
        break;
      case Opcodes.IASTORE:
      case Opcodes.BASTORE:
      case Opcodes.CASTORE:
      case Opcodes.SASTORE:
      case Opcodes.FASTORE:
      case Opcodes.AASTORE:
        pop(3);
        break;
      case Opcodes.LASTORE:
      case Opcodes.DASTORE:
        pop(4);
        break;
      case Opcodes.POP:
      case Opcodes.IFEQ:
      case Opcodes.IFNE:
      case Opcodes.IFLT:
      case Opcodes.IFGE:
      case Opcodes.IFGT:
      case Opcodes.IFLE:
      case Opcodes.IRETURN:
      case Opcodes.FRETURN:
      case Opcodes.ARETURN:
      case Opcodes.TABLESWITCH:
      case Opcodes.LOOKUPSWITCH:
      case Opcodes.ATHROW:
      case Opcodes.MONITORENTER:
      case Opcodes.MONITOREXIT:
      case Opcodes.IFNULL:
      case Opcodes.IFNONNULL:
        pop(1);
        break;
      case Opcodes.POP2:
      case Opcodes.IF_ICMPEQ:
      case Opcodes.IF_ICMPNE:
      case Opcodes.IF_ICMPLT:
      case Opcodes.IF_ICMPGE:
      case Opcodes.IF_ICMPGT:
      case Opcodes.IF_ICMPLE:
      case Opcodes.IF_ACMPEQ:
      case Opcodes.IF_ACMPNE:
      case Opcodes.LRETURN:
      case Opcodes.DRETURN:
        pop(2);
        break;
      case Opcodes.DUP:
        value1 = pop();
        push(value1);
        push(value1);
        break;
      case Opcodes.DUP_X1:
        value1 = pop();
        value2 = pop();
        push(value1);
        push(value2);
        push(value1);
        break;
      case Opcodes.DUP_X2:
        value1 = pop();
        value2 = pop();
        value3 = pop();
        push(value1);
        push(value3);
        push(value2);
        push(value1);
        break;
      case Opcodes.DUP2:
        value1 = pop();
        value2 = pop();
        push(value2);
        push(value1);
        push(value2);
        push(value1);
        break;
      case Opcodes.DUP2_X1:
        value1 = pop();
        value2 = pop();
        value3 = pop();
        push(value2);
        push(value1);
        push(value3);
        push(value2);
        push(value1);
        break;
      case Opcodes.DUP2_X2:
        value1 = pop();
        value2 = pop();
        value3 = pop();
        t4 = pop();
        push(value2);
        push(value1);
        push(t4);
        push(value3);
        push(value2);
        push(value1);
        break;
      case Opcodes.SWAP:
        value1 = pop();
        value2 = pop();
        push(value1);
        push(value2);
        break;
      case Opcodes.IALOAD:
      case Opcodes.BALOAD:
      case Opcodes.CALOAD:
      case Opcodes.SALOAD:
      case Opcodes.IADD:
      case Opcodes.ISUB:
      case Opcodes.IMUL:
      case Opcodes.IDIV:
      case Opcodes.IREM:
      case Opcodes.IAND:
      case Opcodes.IOR:
      case Opcodes.IXOR:
      case Opcodes.ISHL:
      case Opcodes.ISHR:
      case Opcodes.IUSHR:
      case Opcodes.L2I:
      case Opcodes.D2I:
      case Opcodes.FCMPL:
      case Opcodes.FCMPG:
        pop(2);
        push(Opcodes.INTEGER);
        break;
      case Opcodes.LADD:
      case Opcodes.LSUB:
      case Opcodes.LMUL:
      case Opcodes.LDIV:
      case Opcodes.LREM:
      case Opcodes.LAND:
      case Opcodes.LOR:
      case Opcodes.LXOR:
        pop(4);
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        break;
      case Opcodes.FALOAD:
      case Opcodes.FADD:
      case Opcodes.FSUB:
      case Opcodes.FMUL:
      case Opcodes.FDIV:
      case Opcodes.FREM:
      case Opcodes.L2F:
      case Opcodes.D2F:
        pop(2);
        push(Opcodes.FLOAT);
        break;
      case Opcodes.DADD:
      case Opcodes.DSUB:
      case Opcodes.DMUL:
      case Opcodes.DDIV:
      case Opcodes.DREM:
        pop(4);
        push(Opcodes.DOUBLE);
        push(Opcodes.TOP);
        break;
      case Opcodes.LSHL:
      case Opcodes.LSHR:
      case Opcodes.LUSHR:
        pop(3);
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        break;
      case Opcodes.IINC:
        set(intArg, Opcodes.INTEGER);
        break;
      case Opcodes.I2L:
      case Opcodes.F2L:
        pop(1);
        push(Opcodes.LONG);
        push(Opcodes.TOP);
        break;
      case Opcodes.I2F:
        pop(1);
        push(Opcodes.FLOAT);
        break;
      case Opcodes.I2D:
      case Opcodes.F2D:
        pop(1);
        push(Opcodes.DOUBLE);
        push(Opcodes.TOP);
        break;
      case Opcodes.F2I:
      case Opcodes.ARRAYLENGTH:
      case Opcodes.INSTANCEOF:
        pop(1);
        push(Opcodes.INTEGER);
        break;
      case Opcodes.LCMP:
      case Opcodes.DCMPL:
      case Opcodes.DCMPG:
        pop(4);
        push(Opcodes.INTEGER);
        break;
      case Opcodes.GETSTATIC:
        pushDescriptor(stringArg);
        break;
      case Opcodes.PUTSTATIC:
        pop(stringArg);
        break;
      case Opcodes.GETFIELD:
        pop(1);
        pushDescriptor(stringArg);
        break;
      case Opcodes.PUTFIELD:
        pop(stringArg);
        pop();
        break;
      case Opcodes.NEW:
        push(labels.get(0));
        break;
      case Opcodes.NEWARRAY:
        pop();
        switch (intArg) {
          case Opcodes.T_BOOLEAN:
            pushDescriptor("[Z");
            break;
          case Opcodes.T_CHAR:
            pushDescriptor("[C");
            break;
          case Opcodes.T_BYTE:
            pushDescriptor("[B");
            break;
          case Opcodes.T_SHORT:
            pushDescriptor("[S");
            break;
          case Opcodes.T_INT:
            pushDescriptor("[I");
            break;
          case Opcodes.T_FLOAT:
            pushDescriptor("[F");
            break;
          case Opcodes.T_DOUBLE:
            pushDescriptor("[D");
            break;
          case Opcodes.T_LONG:
            pushDescriptor("[J");
            break;
          default:
            throw new IllegalArgumentException("Invalid array type " + intArg);
        }
        break;
      case Opcodes.ANEWARRAY:
        pop();
        pushDescriptor("[" + Type.getObjectType(stringArg));
        break;
      case Opcodes.CHECKCAST:
        pop();
        pushDescriptor(Type.getObjectType(stringArg).getDescriptor());
        break;
      case Opcodes.MULTIANEWARRAY:
        pop(intArg);
        pushDescriptor(stringArg);
        break;
      default:
        throw new IllegalArgumentException("Invalid opcode " + opcode);
    }
    labels = null;
  }
}
